// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

////////////////////////////////////////////////////////////////////////////////
/// CI entrypoint for running Pigeon tests.
///
/// For any use other than CI, use test.dart instead.
////////////////////////////////////////////////////////////////////////////////
import 'dart:io';

import 'package:path/path.dart' as p;

import 'shared/generation.dart';
import 'shared/test_runner.dart';
import 'shared/test_suites.dart';

/// Exits with failure if any tests in [testSuites] are not included in any of
/// the given test [shards].
void _validateTestCoverage(List<List<String>> shards) {
  final Set<String> missing = testSuites.keys.toSet();
  shards.forEach(missing.removeAll);

  if (missing.isNotEmpty) {
    print('The following test suites are not being run on any host:');
    for (final String suite in missing) {
      print('  $suite');
    }
    exit(1);
  }
}

Future<void> _validateGeneratedTestFiles() async {
  final String baseDir = p.dirname(p.dirname(Platform.script.toFilePath()));
  final String repositoryRoot = p.dirname(p.dirname(baseDir));
  final String relativePigeonPath = p.relative(baseDir, from: repositoryRoot);

  print('Validating generated files:');
  print('  Generating test output...');
  final int generateExitCode = await generateTestPigeons(baseDir: baseDir);
  if (generateExitCode != 0) {
    print('Generation failed; see above for errors.');
    exit(generateExitCode);
  }

  print('  Formatting output...');
  final int formatExitCode =
      await formatAllFiles(repositoryRoot: repositoryRoot);
  if (formatExitCode != 0) {
    print('Formatting failed; see above for errors.');
    exit(formatExitCode);
  }

  print('  Checking for changes...');
  final List<String> modifiedFiles = await _modifiedFiles(
      repositoryRoot: repositoryRoot, relativePigeonPath: relativePigeonPath);

  if (modifiedFiles.isEmpty) {
    return;
  }

  print('The following files are not updated, or not formatted correctly:');
  modifiedFiles.map((String line) => '  $line').forEach(print);

  print('\nTo fix run "dart run tool/generate.dart --format" from the pigeon/ '
      'directory, or apply the diff with the command below.\n');

  final ProcessResult diffResult = await Process.run(
    'git',
    <String>['diff', relativePigeonPath],
    workingDirectory: repositoryRoot,
  );
  if (diffResult.exitCode != 0) {
    print('Unable to determine diff.');
    exit(1);
  }
  print('patch -p1 <<DONE');
  print(diffResult.stdout);
  print('DONE');
  exit(1);
}

Future<void> _validateGeneratedExampleFiles() async {
  final String baseDir = p.dirname(p.dirname(Platform.script.toFilePath()));
  final String repositoryRoot = p.dirname(p.dirname(baseDir));
  final String relativePigeonPath = p.relative(baseDir, from: repositoryRoot);

  print('Validating generated files:');
  print('  Generating example output...');

  final int generateExitCode = await generateExamplePigeons();

  if (generateExitCode != 0) {
    print('Generation failed; see above for errors.');
    exit(generateExitCode);
  }

  print('  Formatting output...');
  final int formatExitCode =
      await formatAllFiles(repositoryRoot: repositoryRoot);
  if (formatExitCode != 0) {
    print('Formatting failed; see above for errors.');
    exit(formatExitCode);
  }

  print('  Checking for changes...');
  final List<String> modifiedFiles = await _modifiedFiles(
      repositoryRoot: repositoryRoot, relativePigeonPath: relativePigeonPath);

  if (modifiedFiles.isEmpty) {
    return;
  }

  print(
      'Either messages.dart and messages_test.dart have non-matching definitions or');
  print('the following files are not updated, or not formatted correctly:');
  modifiedFiles.map((String line) => '  $line').forEach(print);

  print('\nTo fix run "dart run tool/generate.dart --format" from the pigeon/ '
      'directory, or apply the diff with the command below.\n');

  final ProcessResult diffResult = await Process.run(
    'git',
    <String>['diff', relativePigeonPath],
    workingDirectory: repositoryRoot,
  );
  if (diffResult.exitCode != 0) {
    print('Unable to determine diff.');
    exit(1);
  }
  print('patch -p1 <<DONE');
  print(diffResult.stdout);
  print('DONE');
  exit(1);
}

Future<List<String>> _modifiedFiles(
    {required String repositoryRoot,
    required String relativePigeonPath}) async {
  final ProcessResult result = await Process.run(
    'git',
    <String>['ls-files', '--modified', relativePigeonPath],
    workingDirectory: repositoryRoot,
  );
  if (result.exitCode != 0) {
    print('Unable to determine changed files.');
    print(result.stdout);
    print(result.stderr);
    exit(1);
  }
  return (result.stdout as String)
      .split('\n')
      .map((String line) => line.trim())
      .where((String line) => line.isNotEmpty)
      .toList();
}

Future<void> main(List<String> args) async {
  // Run most tests on Linux, since Linux tends to be the easiest and cheapest.
  const List<String> linuxHostTests = <String>[
    commandLineTests,
    androidJavaUnitTests,
    androidJavaLint,
    androidKotlinUnitTests,
    androidJavaIntegrationTests,
    androidKotlinIntegrationTests,
  ];
  const List<String> macOSHostTests = <String>[
    iOSObjCUnitTests,
    // Currently these are testing exactly the same thing as
    // macOS*IntegrationTests, so we don't need to run both by default. This
    // should be enabled if any iOS-only tests are added (e.g., for a feature
    // not supported by macOS).
    // iOSObjCIntegrationTests,
    // iOSSwiftIntegrationTests,
    iOSSwiftUnitTests,
    macOSObjCIntegrationTests,
    macOSSwiftUnitTests,
    macOSSwiftIntegrationTests,
  ];
  // Run Windows tests on Windows, since that's the only place they can run.
  const List<String> windowsHostTests = <String>[
    windowsUnitTests,
    windowsIntegrationTests,
  ];

  _validateTestCoverage(<List<String>>[
    linuxHostTests,
    macOSHostTests,
    windowsHostTests,
    // Tests that are deliberately not included in CI:
    <String>[
      // See comments in macOSHostTests:
      iOSObjCIntegrationTests,
      iOSSwiftIntegrationTests,
      // These are Dart unit tests, which are already run by the normal
      // test-dart repo tools command.
      dartUnitTests,
      flutterUnitTests,
    ],
  ]);

  // Ensure that all generated files are up to date. This is run only on Linux
  // both to avoid duplication of work, and to avoid issues if different CI
  // configurations have different setups (e.g., different clang-format versions
  // or no clang-format at all).
  if (Platform.isLinux) {
    // Only run on master, since Dart format can change between versions.
    // TODO(stuartmorgan): Make a more generic way to run this check only on
    // master; this currently won't work for anything but Cirrus.
    if (Platform.environment['CHANNEL'] == 'stable') {
      print('Skipping generated file validation on stable.');
    } else {
      await _validateGeneratedTestFiles();
      await _validateGeneratedExampleFiles();
    }
  }

  final List<String> testsToRun;
  if (Platform.isMacOS) {
    testsToRun = macOSHostTests;
  } else if (Platform.isWindows) {
    testsToRun = windowsHostTests;
  } else if (Platform.isLinux) {
    testsToRun = linuxHostTests;
  } else {
    print('Unsupported host platform.');
    exit(2);
  }

  await runTests(testsToRun);
}
